package cc.idoone.codegen.generator;

import cc.idoone.codegen.config.CodeGenConfig;
import cc.idoone.codegen.table.TableField;
import cc.idoone.codegen.table.TableInfo;
import cc.idoone.codegen.table.TableInfoQuery;
import cc.idoone.codegen.util.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.yaml.snakeyaml.Yaml;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TreeSet;

/**
 * @auther hemj
 * @since 2020/8/25
 */
public class CodeGenerator {
	/**
	 * codegen.yml 配置信息
	 */
	private CodeGenConfig config;
	/**
	 * velocity 模板引擎
	 */
	private VelocityEngine engine;

	private File baseDir;
	private String basePackage;

	public void generate(String conf) throws SQLException, ClassNotFoundException, IOException {
		// 加载 codegen.yml 配置信息
		loadYamlConfig(conf);
		// 初始化 velocity 模板引擎
		initVelocityEngine();
		// 数据表查询工具
		TableInfoQuery query = TableInfoQuery.getInstance(config.getDataSource());

		// ${date} 创建时间
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		String today = sdf.format(new Date());
		String ormFramework = config.getOrmFramework();

		List<String> tableNames = config.getModule().getTables();
		basePackage = config.getModule().getBasePackage();
		baseDir = new File(config.getOutputDir(), basePackage.replace('.', File.separatorChar));
		for (String tableName : tableNames) {
			// table
			TableInfo tableInfo = query.queryTableInfo(tableName);
			if (tableInfo == null)
				continue;
			String entityInstance = tableName;
			if (StringUtils.isNotEmpty(config.getModule().getStripPrefix())) {
				if (tableName.toLowerCase().startsWith(config.getModule().getStripPrefix())) {
					entityInstance = tableName.substring(config.getModule().getStripPrefix().length());
				}
			}
			entityInstance = StringUtils.underlineToCamel(entityInstance);
			String entityName = entityInstance.substring(0, 1).toUpperCase() + entityInstance.substring(1);
			for (TableField field : tableInfo.getFields()) {
				String fill = config.getEntity().getFieldFills().get(field.getName());
				field.setFill(fill);
			}

			VelocityContext ctx = new VelocityContext();
			ctx.put("config", config);
			ctx.put("date", today);
			ctx.put("table", tableInfo);
			ctx.put("entityName", entityName);
			ctx.put("entityInstance", entityInstance);
			ctx.put("queryResultType", config.getMapper().isEnableCamelCase() ? entityName : "Map");

			if (CodeGenConfig.ORM_MYBATIS.equals(config.getOrmFramework())) {
				generateMybatisCodes(ctx);
			} else {
				generateMybatisPlusCodes(ctx);
			}

			System.out.println("");
		}
	}

	private void generateMybatisCodes(VelocityContext ctx) throws IOException {
		// table info
		TableInfo tableInfo = (TableInfo) ctx.get("table");
		String entityName = (String) ctx.get("entityName");
		// entity
		File entityFile = new File(baseDir, "entity/" + entityName + ".java");
		String entityPackage = basePackage + ".entity";
		String entityClass = entityPackage + "." + entityName;
		TreeSet entityImports = new TreeSet();

		entityImports.add("java.io.Serializable");
		if (config.getEntity().getLombokEnabled()) {
			entityImports.add("lombok.Data");
			entityImports.add("lombok.experimental.Accessors");
		}
		for (TableField field : tableInfo.getFields()) {
			if (field.getColumnType().getFullName() != null)
				entityImports.add(field.getColumnType().getFullName());
		}
		ctx.put("entityPackage", entityPackage);
		ctx.put("entityImports", entityImports);
		ctx.put("resultType", config.getMapper().isEnableEntityAlias() ? entityName : entityClass);
		generateFile(ctx, "entity.java.vm", entityFile);

		// mapper
		File mapperFile = new File(baseDir, "mapper/" + entityName + "Mapper.java");
		String mapperPackage = basePackage + ".mapper";
		String mapperClass = basePackage + ".mapper." + entityName + "Mapper";
		TreeSet mapperImports = new TreeSet();
		mapperImports.add(entityClass);
		mapperImports.add("org.apache.ibatis.annotations.Param");
		mapperImports.add("org.springframework.stereotype.Repository");
		ctx.put("mapperPackage", mapperPackage);
		ctx.put("mapperImports", mapperImports);
		generateFile(ctx, "mapper.java.vm", mapperFile);

		// mapper.xml
		File xmlFile = new File(baseDir, "mapper/" + entityName + "Mapper.xml");
		generateFile(ctx, "mapper.xml.vm", xmlFile);

		// service
		File serviceFile = new File(baseDir, "service/I" + entityName + "Service.java");
		String servicePackage = basePackage + ".service";
		String serviceClass = basePackage + ".service.I" + entityName + "Service";
		TreeSet serviceImports = new TreeSet();
		serviceImports.add(entityClass);
		ctx.put("servicePackage", servicePackage);
		ctx.put("serviceImports", serviceImports);
		ctx.put("serviceSuperClassName", "IService");
		generateFile(ctx, "service.java.vm", serviceFile);

		// service.impl
		File serviceImplFile = new File(baseDir, "service/impl/" + entityName + "ServiceImpl.java");
		TreeSet serviceImplImports = new TreeSet();
		serviceImplImports.add("org.springframework.beans.factory.annotation.Autowired");
		serviceImplImports.add("org.springframework.stereotype.Service");
		serviceImplImports.add("java.lang.Integer");
		serviceImplImports.add(entityClass);
		serviceImplImports.add(mapperClass);
		serviceImplImports.add(serviceClass);
		ctx.put("serviceImplPackage", servicePackage + ".impl");
		ctx.put("serviceImplImports", serviceImplImports);
		ctx.put("serviceImplSuperClassName", "ServiceImpl");
		generateFile(ctx, "serviceImpl.java.vm", serviceImplFile);

		// facade
		File facadeFile = new File(baseDir, "facade/I" + entityName + "Facade.java");
		String facadePackage = basePackage + ".facade";
		String facadeClass = basePackage + ".facade.I" + entityName + "Facade";
		TreeSet facadeImports = new TreeSet();
		ctx.put("facadePackage", facadePackage);
		ctx.put("facadeImports", facadeImports);
		ctx.put("facadeSuperClassName", "IFacade");
		generateFile(ctx, "facade.java.vm", facadeFile);

		// facade.impl
		File facadeImplFile = new File(baseDir, "facade/impl/" + entityName + "FacadeImpl.java");
		TreeSet facadeImplImports = new TreeSet();
		facadeImplImports.add("org.springframework.beans.factory.annotation.Autowired");
		facadeImplImports.add("org.springframework.stereotype.Service");
		facadeImplImports.add(serviceClass);
		facadeImplImports.add(facadeClass);
		ctx.put("facadeImplPackage", facadePackage + ".impl");
		ctx.put("facadeImplImports", facadeImplImports);
		ctx.put("facadeImplSuperClassName", "FacadeImpl");
		generateFile(ctx, "facadeImpl.java.vm", facadeImplFile);

		// controller
		File controllerFile = new File(baseDir, "controller/" + entityName + "Controller.java");
		String controllerPackage = basePackage + ".controller";
		TreeSet controllerImports = new TreeSet();
		controllerImports.add("org.springframework.beans.factory.annotation.Autowired");
		controllerImports.add("org.springframework.web.bind.annotation.DeleteMapping");
		controllerImports.add("org.springframework.web.bind.annotation.GetMapping");
		controllerImports.add("org.springframework.web.bind.annotation.PathVariable");
		controllerImports.add("org.springframework.web.bind.annotation.PostMapping");
		controllerImports.add("org.springframework.web.bind.annotation.RequestBody");
		controllerImports.add("org.springframework.web.bind.annotation.RequestMapping");
		controllerImports.add("org.springframework.web.bind.annotation.RestController");
		controllerImports.add(entityClass);
		controllerImports.add(serviceClass);
		ctx.put("controllerPackage", controllerPackage);
		ctx.put("controllerImports", controllerImports);
		generateFile(ctx, "controller.java.vm", controllerFile);
	}

	private void generateMybatisPlusCodes(VelocityContext ctx) throws IOException {
		String baseMapper = config.getMybatisplus().getBaseMapperClassName();
		String baseMapperName = baseMapper.substring(baseMapper.lastIndexOf('.') + 1);
		// table info
		TableInfo tableInfo = (TableInfo) ctx.get("table");
		String entityName = (String) ctx.get("entityName");
		// entity
		File entityFile = new File(baseDir, "entity/" + entityName + ".java");
		String entityPackage = basePackage + ".entity";
		String entityClass = entityPackage + "." + entityName;
		TreeSet entityImports = new TreeSet();

		entityImports.add("com.baomidou.mybatisplus.annotation.IdType");
		entityImports.add("com.baomidou.mybatisplus.annotation.TableId");
		entityImports.add("com.baomidou.mybatisplus.annotation.TableName");
		entityImports.add("java.io.Serializable");
		if (config.getEntity().getLombokEnabled()) {
			entityImports.add("lombok.Data");
			entityImports.add("lombok.experimental.Accessors");
		}
		for (TableField field : tableInfo.getFields()) {
			if (field.getColumnType().getFullName() != null)
				entityImports.add(field.getColumnType().getFullName());
			if (StringUtils.isNotEmpty(field.getFill())) {
				entityImports.add("com.baomidou.mybatisplus.annotation.TableField");
				entityImports.add("com.baomidou.mybatisplus.annotation.FieldFill");
			}
			if (!field.getName().equals(field.getPropertyName()))
				entityImports.add("com.baomidou.mybatisplus.annotation.TableField");
			if (field.getName().equals(config.getEntity().getLogicDeleteField()))
				entityImports.add("com.baomidou.mybatisplus.annotation.TableLogic");
		}
		ctx.put("entityPackage", entityPackage);
		ctx.put("entityImports", entityImports);
		ctx.put("resultType", config.getMapper().isEnableEntityAlias() ? entityName : entityClass);
		generateFile(ctx, "entity.java.vm", entityFile);

		// mapper
		File mapperFile = new File(baseDir, "mapper/" + entityName + "Mapper.java");
		String mapperPackage = basePackage + ".mapper";
		String mapperClass = basePackage + ".mapper." + entityName + "Mapper";
		TreeSet mapperImports = new TreeSet();
		mapperImports.add("com.baomidou.mybatisplus.extension.plugins.pagination.Page");
		mapperImports.add("org.apache.ibatis.annotations.Param");
		mapperImports.add("java.util.List");
		mapperImports.add("java.util.Map");
		mapperImports.add(baseMapper);
		mapperImports.add(entityClass);
		ctx.put("mapperPackage", mapperPackage);
		ctx.put("mapperImports", mapperImports);
		ctx.put("mapperSuperClassName", baseMapperName);
		generateFile(ctx, "mapper.java.vm", mapperFile);

		// mapper.xml
		File xmlFile = new File(baseDir, "mapper/" + entityName + "Mapper.xml");
		generateFile(ctx, "mapper.xml.vm", xmlFile);

		// service
		File serviceFile = new File(baseDir, "service/I" + entityName + "Service.java");
		String servicePackage = basePackage + ".service";
		String serviceClass = basePackage + ".service.I" + entityName + "Service";
		TreeSet serviceImports = new TreeSet();
		serviceImports.add("com.baomidou.mybatisplus.extension.service.IService");
		serviceImports.add("com.alibaba.fastjson.JSONObject");
		serviceImports.add(entityClass);
		ctx.put("servicePackage", servicePackage);
		ctx.put("serviceImports", serviceImports);
		ctx.put("serviceSuperClassName", "IService");
		generateFile(ctx, "service.java.vm", serviceFile);

		// service.impl
		File serviceImplFile = new File(baseDir, "service/impl/" + entityName + "ServiceImpl.java");
		TreeSet serviceImplImports = new TreeSet();
		serviceImplImports.add("com.baomidou.mybatisplus.extension.service.impl.ServiceImpl");
		serviceImplImports.add("org.springframework.stereotype.Service");
		serviceImplImports.add("com.alibaba.fastjson.JSONObject");
		serviceImplImports.add("com.baomidou.mybatisplus.extension.plugins.pagination.Page");
		serviceImplImports.add("org.apache.commons.lang3.StringUtils");
		serviceImplImports.add("java.util.List");
		if (!config.getMapper().isEnableCamelCase()) {
			serviceImplImports.add("java.util.Map");
		}
		if (StringUtils.isNotEmpty(config.getMybatisplus().getPageHelperClassName())) {
			serviceImplImports.add(config.getMybatisplus().getPageHelperClassName());
		}
		serviceImplImports.add(entityClass);
		serviceImplImports.add(mapperClass);
		serviceImplImports.add(serviceClass);
		ctx.put("serviceImplPackage", servicePackage + ".impl");
		ctx.put("serviceImplImports", serviceImplImports);
		ctx.put("serviceImplSuperClassName", "ServiceImpl");
		generateFile(ctx, "serviceImpl.java.vm", serviceImplFile);

		// facade
		File facadeFile = new File(baseDir, "facade/I" + entityName + "Facade.java");
		String facadePackage = basePackage + ".facade";
		String facadeClass = basePackage + ".facade.I" + entityName + "Facade";
		TreeSet facadeImports = new TreeSet();
		ctx.put("facadePackage", facadePackage);
		ctx.put("facadeImports", facadeImports);
		ctx.put("facadeSuperClassName", "IFacade");
		generateFile(ctx, "facade.java.vm", facadeFile);

		// facade.impl
		File facadeImplFile = new File(baseDir, "facade/impl/" + entityName + "FacadeImpl.java");
		TreeSet facadeImplImports = new TreeSet();
		facadeImplImports.add("org.springframework.stereotype.Service");
		facadeImplImports.add(facadeClass);
		ctx.put("facadeImplPackage", facadePackage + ".impl");
		ctx.put("facadeImplImports", facadeImplImports);
		ctx.put("facadeImplSuperClassName", "FacadeImpl");
		generateFile(ctx, "facadeImpl.java.vm", facadeImplFile);

		// controller
		File controllerFile = new File(baseDir, "controller/" + entityName + "Controller.java");
		String controllerPackage = basePackage + ".controller";
		TreeSet controllerImports = new TreeSet();
		controllerImports.add("org.springframework.beans.factory.annotation.Autowired");
		controllerImports.add("org.springframework.web.bind.annotation.DeleteMapping");
		controllerImports.add("org.springframework.web.bind.annotation.GetMapping");
		controllerImports.add("org.springframework.web.bind.annotation.PathVariable");
		controllerImports.add("org.springframework.web.bind.annotation.PostMapping");
		controllerImports.add("org.springframework.web.bind.annotation.RequestBody");
		controllerImports.add("org.springframework.web.bind.annotation.RequestMapping");
		controllerImports.add("org.springframework.web.bind.annotation.RestController");
		controllerImports.add(entityClass);
		controllerImports.add(serviceClass);
		ctx.put("controllerPackage", controllerPackage);
		ctx.put("controllerImports", controllerImports);
		generateFile(ctx, "controller.java.vm", controllerFile);
	}

	/**
	 * 加载配置信息
	 */
	private void loadYamlConfig(String conf) {
		InputStream in = ClassLoader.getSystemResourceAsStream(StringUtils.isNotEmpty(conf) ? conf : "codegen.yml");
		Yaml yaml = new Yaml();
		config = yaml.loadAs(in, CodeGenConfig.class);
	}

	/**
	 * 设置模版引擎，主要指向获取模版路径
	 */
	private void initVelocityEngine() {
		if (engine == null) {
			Properties p = new Properties();
			p.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
			p.setProperty(Velocity.INPUT_ENCODING, "UTF-8");
			p.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");

			// 设置velocity资源加载方式为file
			// p.setProperty(Velocity.FILE_RESOURCE_LOADER_CACHE, "true");
			// p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, ".");

			// 设置velocity资源加载方式为class
			p.setProperty(Velocity.RESOURCE_LOADER, "class");
			p.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
			p.setProperty("class.resource.loader.cache", "true");

			// 设置velocity资源加载方式为jar
			// p.setProperty("resource.loader", "jar");
			// p.setProperty("jar.resource.loader.class", "org.apache.velocity.runtime.resource.loader.JarResourceLoader");
			// p.setProperty("jar.resource.loader.cache", "true");
			// p.setProperty("jar.resource.loader.path", "jar:file:WebRoot/WEB-INF/lib/vm.jar");

			engine = new VelocityEngine(p);
		}
	}

	/**
	 * 使用模板转化生成文件
	 */
	private void generateFile(VelocityContext context, String templatePath, File file) throws IOException {
		Template template = engine.getTemplate("/templates/" + config.getOrmFramework() + "/" + templatePath, "UTF-8");
		if (!file.getParentFile().exists()) {
			if (file.getParentFile().mkdirs()) {
				System.out.println("创建目录 " + file.getParentFile().getAbsolutePath());
			} else {
				System.err.println("创建目录失败 " + file.getParentFile().getAbsolutePath());
				return;
			}
		}
		FileOutputStream fos = new FileOutputStream(file);
		BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
		template.merge(context, writer);
		writer.close();
		System.out.println("生成文件 " + file.getAbsolutePath());
	}

}
